/*
 * This file is part of Catfish.
 * Copyright (C) 2010 Namazu Studios LLC
 *     
 * Catfish is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as 
 * published by the Free Software Foundation, either version 3 of 
 * the License, or (at your option) any later version.
 * 
 * Catfish is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with Catfish.  If not, please visit:
 *  
 *  http://www.gnu.org/licenses/
 *  
 */
package com.namazustudios.catfish;

import java.util.List;
import java.util.Map;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Transaction;
import com.namazustudios.catfish.annotation.Entity;
import com.namazustudios.catfish.callback.CriticalSection;
import com.namazustudios.catfish.exception.CacheMissException;

public interface Catfish {

	/**
	 * Shortcut to begin a transaction.
	 * 
	 * @return a new transaction.
	 */
	public abstract Transaction beginTransaction();

	/**
	 * Gets the collection of objects from the datastore. Each Key passed must
	 * correspond to the given class. This guarantees that each result returned
	 * will be an instance of the given class.
	 * 
	 * @param <T>
	 * @param cls
	 * @param keys
	 * @return
	 */
	public abstract <T> Map<Key, T> cacheGet(Class<T> cls, Iterable<Key> keys);

	/**
	 * Gets the collection of objects from the datastore. Each Key passed must
	 * correspond to the given class. This guaranees that each result returned
	 * will be an instance of the given class.
	 * 
	 * This attempts to pull from the cache as many elements as it can.
	 * 
	 * @param <T>
	 * @param cls
	 * @param keys
	 * @return
	 */
	public abstract <T> Map<Key, T> cacheGet(Class<T> cls, Key... keys);

	/**
	 * Gets the single object from the memecache. If the object is not cached,
	 * it will raise an exception.
	 * 
	 * @param <T>
	 * @param cls
	 * @param key
	 * @return
	 * @throws CacheMissException
	 */
	public abstract <T> T cacheGet(Class<T> cls, Key key)
			throws CacheMissException;

	/**
	 * Gets teh collection of objects from the datastore. Each Key passed must
	 * correspond to the given class. This guarantees that each result returned
	 * will be an instance of the given class.
	 * 
	 * @param <T>
	 * @param cls
	 * @param keys
	 * @return
	 */
	public abstract Map<Key, Object> cacheGet(Iterable<Key> keys);

	/**
	 * Gets the collection of objects from the datastore. Each Key passed must
	 * correspond to the given class. This guaranees that each result returned
	 * will be an instance of the given class.
	 * 
	 * This attempts to pull from the cache as many elements as it can.
	 * 
	 * @param <T>
	 * @param cls
	 * @param keys
	 * @return
	 */
	public abstract Map<Key, Object> cacheGet(Key... keys);

	/**
	 * Gets the single object from the datastore. If the object is cached, it
	 * looks there first and returns it.
	 * 
	 * @param <T>
	 * @param cls
	 * @param key
	 * @return
	 * @throws EntityNotFoundException
	 */
	public abstract Object cacheGet(Key key) throws CacheMissException;

	/**
	 * Deletes the given objects from the datastore. Each object's Class must be
	 * annotated @Entity and have a single @Key field. If appropriate, They are
	 * also removed from the memory cache.
	 * 
	 * Please note that this does not delete child entities of the given object,
	 * but only deletes the objects passed.
	 * 
	 * @param objects
	 */
	public abstract void delete(Iterable<? extends Object> objects);

	/**
	 * Performs a bulk delete of the given objects. Each object's Class must be
	 * annotated @Entity and have a single @Key field. If appropriate, They are
	 * also removed from the memory cache.
	 * 
	 * Please note that this does not delete child entities of the given object,
	 * but only deletes the objects passed.
	 * 
	 * @param objects
	 */
	public abstract <T> void delete(T... objects);

	/**
	 * Bulk deletes the given objects. This uses the given transaction to
	 * perform the delete. Each object's Class must be annotated @Entity and
	 * have a single @Key field. If appropriate, they are also removed from the
	 * memory cache.
	 * 
	 * @param transaction
	 * @param objects
	 */
	public abstract void delete(Transaction transaction,
			Iterable<? extends Object> objects);

	/**
	 * Bulk deletes the dgiven objects. This uses the given transaction to
	 * perform the delete. Each object's Class must be annotated @Entity and
	 * have a single @Key field. If appropraite, they are also removed from the
	 * cache.
	 * 
	 * @param transaction
	 * @param objects
	 */
	public abstract <T> void delete(Transaction transaction, T... objects);

	/**
	 * Gets the collection of objects from the datastore. Each Key passed must
	 * correspond to the given class. This guarantees that each result returned
	 * will be an instance of the given class.
	 * 
	 * @param <T>
	 * @param cls
	 * @param keys
	 * @return
	 */
	public abstract <T> Map<Key, T> get(Class<T> cls, Iterable<Key> keys);

	/**
	 * Gets the collection of objects from the datastore. Each Key passed must
	 * correspond to the given class. This guaranees that each result returned
	 * will be an instance of the given class.
	 * 
	 * This attempts to pull from the cache as many elements as it can.
	 * 
	 * @param <T>
	 * @param cls
	 * @param keys
	 * @return
	 */
	public abstract <T> Map<Key, T> get(Class<T> cls, Key... keys);

	/**
	 * Gets the single object from the datastore. If the object is cached, it
	 * looks there first and returns it.
	 * 
	 * @param <T>
	 * @param cls
	 * @param key
	 * @return
	 * @throws EntityNotFoundException
	 */
	public abstract <T> T get(Class<T> cls, Key key)
			throws EntityNotFoundException;

	/**
	 * Gets the given set of objects from the datastore. If the object is
	 * cached, it looks there first.
	 * 
	 * @param <T>
	 * @param cls
	 * @param transaction
	 * @param keys
	 * @return
	 */
	public abstract <T> Map<Key, T> get(Class<T> cls, Transaction transaction,
			Iterable<Key> keys);

	/**
	 * Gets the given set of objects from the datastore. If the object is
	 * cached, it looks there first.
	 * 
	 * @param <T>
	 * @param cls
	 * @param transaction
	 * @param keys
	 * @return
	 * @throws EntityNotFoundException
	 */
	public abstract <T> Map<Key, T> get(Class<T> cls, Transaction transaction,
			Key... keys);

	/**
	 * Gets the given object from the datastore. If the object is cached, it
	 * looks there first.
	 * 
	 * @param <T>
	 * @param cls
	 * @param tranaction
	 * @param key
	 * @return
	 * @throws EntityNotFoundException
	 */
	public abstract <T> T get(Class<T> cls, Transaction tranaction, Key key)
			throws EntityNotFoundException;

	/**
	 * Bulk gets the given keys from the datastore. If the object is cached, it
	 * looks there first.
	 * 
	 * @param keys
	 * @return
	 */
	public abstract Map<Key, Object> get(Iterable<Key> keys);

	/**
	 * Gets the given set of keys from the datastore. If the object is cached,
	 * it looks their first.
	 * 
	 * @param keys
	 * @return
	 */
	public abstract Map<Key, Object> get(Key... keys);

	/**
	 * Gets the given object from the datastore. If the object is cached, it
	 * looks there first.
	 * 
	 * @param key
	 * @return
	 * @throws EntityNotFoundException
	 */
	public abstract Object get(Key key) throws EntityNotFoundException;

	/**
	 * Gets the given keys.
	 * 
	 * @param transaction
	 * @param keys
	 * @return
	 */
	public abstract Map<Key, Object> get(Transaction transaction,
			Iterable<Key> keys);

	/**
	 * Gets the given keys with the provided transaction. All keys must be part
	 * of the same entity group.
	 * 
	 * @return
	 */
	public abstract Map<Key, Object> get(Transaction transaction, Key... keys);

	/**
	 * Gets the given object.
	 * 
	 * @param transaction
	 * @param key
	 * @return
	 * @throws EntityNotFoundException
	 */
	public abstract Object get(Transaction transaction, Key key)
			throws EntityNotFoundException;

	/**
	 * Gets the class associated with the given Key.
	 * 
	 * @param key
	 *            they key
	 * @return the fully formed key for the object
	 */
	public abstract Class<?> getClass(Key key);

	/**
	 * Gets the class associated with the given kind.
	 * 
	 * @param kind
	 *            the kind
	 * @return the kind of object
	 */
	public abstract Class<?> getClass(String kind);

	/**
	 * Gets the current DataSource on the stack. This instance will use it for
	 * all interaction with the data store.
	 * 
	 * @return the DataSource currently in use
	 */
	public abstract DataSource getDataSource();

	/**
	 * Gets the datastore service used by this catfish instance.
	 * 
	 * @return the datastore service instance
	 */
	public abstract DatastoreService getDatastore();

	/**
	 * Gets the Key for the given object, or null if the object has not been set
	 * yet.
	 * 
	 * @param object
	 *            the object.
	 * @return
	 */
	public abstract Key getKey(Object object);

	/**
	 * Gets the kind associated with the given class.
	 * 
	 * @param cls
	 *            the class
	 * @return
	 */
	public abstract String getKind(Class<?> cls);

	/**
	 * Gets the Kind associated with the given object.
	 * 
	 * @param object
	 *            the object
	 * @return
	 */
	public abstract String getKind(Object object);

	/**
	 * Instantiates the object with the given class and injecs the appropriate
	 * members.
	 * 
	 * @param <T>
	 * @param cls
	 * @return
	 */
	public abstract <T> T instantiate(Class<T> cls);

	/**
	 * Instanties the object with the given class and key. The class must be
	 * regitered to this instance.
	 * 
	 * @param <T>
	 * @param cls
	 * @param key
	 * @return
	 */
	public abstract <T> T instantiate(Class<T> cls, Key key);

	/**
	 * Instantiates the object with the given Key.
	 * 
	 * @param key
	 * @return
	 */
	public abstract Object instantiate(Key key);

	/**
	 * Returns true if the given Class is annotated with the @Cacheable
	 * annotation>
	 * 
	 * @param cls
	 * @return
	 */
	public abstract boolean isCacheable(Class<?> cls);

	/**
	 * Returns true if the given object is a cacheable object.
	 * 
	 * @param object
	 *            the object
	 * @return
	 */
	public abstract boolean isCacheable(Object object);

	/**
	 * Checks if caching is enabled. If this is true, then entities will be
	 * cached in memcache when they are fetched from the datastore. Once they
	 * are fetched, they will remain as long as possible at the mercy of the
	 * memcache service and subsequent Gets will pull attempt to pull the data
	 * from the cache. Note that doing makes no guarantees as to the consistency
	 * and this should be changed when using a transaction. For example the
	 * following will epically fail to guarantee any consistency because the
	 * object returned by get() will not necessairly come from the datastore.
	 * 
	 * <code>
	 * Transaction txn = catfish.beginTransaction();
	 * 
	 * try {
	 *     SomeType obj = catfish.get(SomeType.class, someKey);
	 *     
	 *     obj.foo();
	 *     obj.bar();
	 *     catfish.put(obj);
	 *     
	 *     txn.commit();
	 * } finally {
	 *     if (txn.isActive() {
	 *         txn.rollback();
	 *     }
	 * }
     * </code>
	 * 
	 * Instead, this should be used:
	 * 
	 * <code>
	 * 
	 * boolean caching = catfish.isCachingEnabled();
	 * Transaction txn = catfish.beginTransaction();
	 * 
	 * catfish.setCachingEnabled(false);
	 * 
	 * try {
	 *     SomeType obj = catfish.get(SomeType.class, someKey);
	 *     
	 *     obj.foo();
	 *     obj.bar();
	 *     catfish.put(obj);
	 *     
	 *     txn.commit();
	 * } finally {
	 * 
	 *     if (txn.isActive() {
	 *         txn.rollback();
	 *     }
	 * 
	 *     catfish.setCachingEnabled(caching);
	 * }
     * </code>
	 * 
	 * Other important notes:
	 * 
	 * This will not prevent Catfish from invalidating cache entries. If you
	 * execute a put or delete, it will still expire the objects' keys from the
	 * cache automatically.
	 * 
	 * This is a thread local setting, and only applies to objects whose types
	 * are both Serializable and annotated with the @Cacheable annotation.
	 * 
	 * @return
	 */
	public abstract boolean isCachingEnabled();

	/**
	 * Gets a marshaller appropriate for marshalling objects to Entities.
	 * 
	 * @return returns a marshaller appropriate for marshalling instances of
	 *         objcts with this catfish.
	 */
	public abstract Marshaller marshaller();

	/**
	 * Creates a key for the given class with the given parent.
	 * 
	 * @param name
	 * @return a freshly instantiated
	 */
	public abstract Key newKey(Class<?> cls, Key parent, String name);

	/**
	 * Creates a key for the given class.
	 * 
	 * @param name
	 * @return fresly instantiated key
	 */
	public abstract Key newKey(Class<?> cls, String name);

	/**
	 * Pops the current DataSource off of the stack of data sources. The current
	 * data source will receive a call back indicating it was popped. If there
	 * are none on the stack, a warning will appear in the logs.
	 * 
	 * Each DataSource is stored on a thread-local stack.
	 */
	public abstract void popSource();

	/**
	 * Pushes the given PooledDataSource onto the stack of DataSources. The new
	 * data source will receive a callback before it is pushed on the stack. The
	 * DataSource passed in will be stored on a thread local stack.
	 * 
	 * @param source
	 */
	public abstract void pushSource(StackedDataSource source);

	/**
	 * Puts the given objects in to the datastore. Each object's class must be
	 * annotated with the @Entity annotation.
	 * 
	 * @param <T>
	 * @param objects
	 * @return
	 */
	public abstract <T> List<Key> put(Iterable<? extends T> objects);

	/**
	 * Puts the given object into the datastore. Upon doing so, the objects are
	 * deleted from the memory cache.
	 * 
	 * @param <T>
	 * @param object
	 * @return
	 */
	public abstract <T> T put(T object);

	/**
	 * Puts the objects into the datastore.
	 * 
	 * @param <T>
	 * @param objects
	 * @return
	 */
	public abstract <T> List<Key> put(T... objects);

	/**
	 * Puts the given set of objects into the datastore. Upon doing so, the
	 * objects are deleted from the memory cache.
	 * 
	 * @param <T>
	 * @param transaction
	 * @param objects
	 * @return
	 */
	public abstract <T> List<Key> put(Transaction transaction,
			Iterable<T> objects);

	/**
	 * Puts the given set of objcts into the datastore. Upon doing so, the
	 * objects are deleted from the memory cache.
	 * 
	 * @param <T>
	 * @param ransaction
	 * @param object
	 * @return
	 */
	public abstract <T> T put(Transaction ransaction, T object);

	/**
	 * Puts the given objects into the datastore.
	 * 
	 * @param transaction
	 * @param objects
	 * @return
	 */
	public abstract <T> List<Key> put(Transaction transaction, T... objects);

	/**
	 * Performs a recursive delete. This searches for all child entities and
	 * delete them as well. This uses a transaction to ensure the entire entity
	 * group or sub entity group is deleted. Each object must be part of the
	 * same entity group or else this will throw an exception.
	 * 
	 * @param objects
	 */
	public abstract void recursiveDelete(Iterable<? extends Object> object);

	/**
	 * Performs a recursive delete. This searches for all child entities and
	 * delete them as well. This uses a transaction to ensure the entire entity
	 * group or sub entity group is deleted. Each object must be part of the
	 * same entity group or else this will throw an exception.
	 * 
	 * @param objects
	 *            the objects to delete
	 */
	public abstract <T> void recursiveDelete(T... objects);

	/**
	 * Performs a recursive delete in the given transaction. A recursive delete
	 * will delete all child entities as well as the given entity. Each object
	 * must be part of the same entity group or else this will throw an
	 * exception.
	 * 
	 * @param transaction
	 * @param object
	 */
	public abstract void recursiveDelete(Transaction transaction,
			Iterable<? extends Object> object);

	/**
	 * Performs a recursive delete in the given transaction. A recursive delete
	 * will delete all child entities as well as the given entity. Each object
	 * must be part of the same entity group or else this will throw an
	 * exception.
	 * 
	 * @param transaction
	 * @param object
	 */
	public abstract <T> void recursiveDelete(Transaction transaction,
			T... objects);

	/**
	 * Sets the caching enabled flag for this thread only. Useful if you are
	 * using a transaction and you wish to guarantee consistency.
	 * 
	 * If this is set to false, it guarantees that all gets are performed as
	 * direct fetches from the datastore.
	 * 
	 * @param cachingEnabled
	 */
	public abstract void setCachingEnabled(boolean cachingEnabled);

	/**
	 * Calls the given callable in its own transaction. This will attempt to
	 * retry the transaction for a certain number of times before giving up and
	 * throwing the appropraite exception type (TransactionTimeoutException).
	 * 
	 * In addition to properly managing hte state of the transaction, this
	 * method will also disable cached reads for the execution of the critical
	 * section of code. When the critical section returns, it will restore the
	 * state of cached gets to its original state.
	 * 
	 * @param <T>
	 * @param callback
	 * @return
	 * @throws Exception
	 */
	public abstract <T, E extends Throwable> T transaction(
			CriticalSection<T, E> callback) throws E;

	/**
	 * Gets an Unmarshaller object which can be used to unmarshalling Entities
	 * into objects.
	 * 
	 * @return an Unmarshaller
	 */
	public abstract Unmarshaller unmarshaller();

	/**
	 * Defines some utility methods for all instances of Catfish.
	 * 
	 * @author patricktwohig
	 * 
	 */
	public static class Utility {

		/**
		 * Gets the Kind of the class passed by reading the annotation.
		 * 
		 * @param cls
		 *            the class
		 * @return a String indicating the Kind associated with the class
		 */
		public static final String getKind(Class<?> cls) {
			if (cls == null) {
				throw new IllegalArgumentException("Class must not be null.");
			} else if (cls.isAnnotationPresent(Entity.class)) {
				String kind = cls.getAnnotation(Entity.class).kind();
				return kind.trim().isEmpty() ? cls.getSimpleName() : kind;
			} else {
				throw new CatfishException("Class " + cls
						+ " is missing @Entity annotation.");
			}
		}

		/**
		 * Gets the Kind of the object passed by reading the annotaion.
		 * 
		 * @param object
		 *            the object
		 * 
		 * @return a String indicating the Kind associated with the object
		 */
		public static final String getKind(Object object) {
			if (object == null) {
				throw new IllegalArgumentException("Object must not be null.");
			} else {
				return getKind(object.getClass());
			}
		}

	}

}